// ==UserScript==
// @name         XMLHttpRequest Network Activity Logger
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Logs all XMLHttpRequest network activities in a web page.
// @author       YourName
// @match        *://*/*
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const originalXMLHttpRequest = window.XMLHttpRequest;
    let xhrRequestCounter = 0;
    let activeXhrCount = 0;
    const NETWORK_ACTIVITY_LOG_KEY = 'networkActivityLog';



    // 定义一个函数来安全地存储复杂对象
    window.setNetworkActivityLog = function (logData) {
        const serializableLogData = JSON.stringify(logData);
        localStorage.setItem(NETWORK_ACTIVITY_LOG_KEY, serializableLogData);
    }

    // 定义一个函数来安全地读取复杂对象
    window.getNetworkActivityLog = function () {
        const logDataStr = localStorage.getItem(NETWORK_ACTIVITY_LOG_KEY, '{}');
        return JSON.parse(logDataStr);
    }

  function simpleHash(str) {
      let hash = 0;
      for (let i = 0; i < str.length; i++) {
          const char = str.charCodeAt(i);
          hash = ((hash << 5) - hash) + char;
          hash |= 0; // 将 hash 转换为 32bit 整数
      }
      return hash;
  }

  function generateUniqueId(url, headers, body) {
      const combinedStr = `${url}${JSON.stringify(headers)}${JSON.stringify(body)}`;
      return simpleHash(combinedStr);
  }

    window.networkActivityLog =  window.getNetworkActivityLog();

    function ModifiedXMLHttpRequest() {
        const xhr = new originalXMLHttpRequest();
        const requestDetails = {
            method: null,
            url: null,
            contentType: null,
            async: true,
            headers: {},
            body: null,
            uniqueId: null,
            startTime: null
        };
        let responseDetails = {};

        xhr.open = function(method, url, async = true) {
            requestDetails.method = method;
            requestDetails.url = url;
            requestDetails.async = async;
            originalXMLHttpRequest.prototype.open.call(this, method, url, async);
        };

        xhr.setRequestHeader = function(header, value) {
            if (header.toLowerCase() === 'content-type') {
                requestDetails.contentType = value;
            }
            requestDetails.headers[header] = value;
            originalXMLHttpRequest.prototype.setRequestHeader.call(this, header, value);
        };

        xhr.onreadystatechange = function() {
            if (this.readyState === 4) {
                const contentType = this.getResponseHeader('Content-Type') || '';
                if (this.responseType === 'blob') {
                    readBlobResponse(this.response, contentType);
                } else {
                    const parsedData = parseByContentType(this.responseText, contentType);
                    updateResponseDetails(parsedData);
                }
                activeXhrCount--;
                // console.log(`activeXhrCount: ${activeXhrCount}`)
                if (activeXhrCount === 0) {
                    console.log(`All XHR requests completed. Network activity(${Object.keys(window.networkActivityLog).length}):`, window.networkActivityLog);
                    downloadNetworkActivityLog();
                }
            }
        };

        function readBlobResponse(blob, contentType) {
            const reader = new FileReader();
            reader.onload = () => {
                const data = reader.result;
                const parsedData = parseByContentType(data, contentType);
                updateResponseDetails(parsedData);
            };
            reader.readAsText(blob);
        }

        function parseByContentType(data, contentType) {
            if (contentType.includes('application/json')) {
                return JSON.parse(data);
            } else if (contentType.includes('text/') || contentType.includes('xml')) {
                return data;
            } else if (contentType.includes('image/') || contentType.includes('video/') || contentType.includes('audio/')) {
                return URL.createObjectURL(new Blob([data]));
            } else {
                return data;
            }
        }

        function updateResponseDetails(parsedData) {
            let responsebody = parsedData.hasOwnProperty('data') ? parsedData.data : parsedData;
            responseDetails = {
                status: xhr.status,
                statusText: xhr.statusText,
                headers: xhr.getAllResponseHeaders(),
                responseType:  `${requestDetails.contentType}`, //xhr.responseType,
                body: responsebody,
                responseURL: xhr.responseURL,
                duration: new Date() - requestDetails.startTime
            };
            const xhrId = generateUniqueId(requestDetails.url, requestDetails.headers, requestDetails.body);
            requestDetails.xhrId = xhrId
            window.networkActivityLog[`${requestDetails.url}?xhr-uniqueId=${xhrId}`] = { request: requestDetails, response: responseDetails };
            window.setNetworkActivityLog(window.networkActivityLog);
            console.log(`%cXHR Request #${xhrId}:`, 'color: orange; font-weight: bold;', ` ${requestDetails.url} ${requestDetails.method} ${requestDetails.contentType}\n`, requestDetails);
            console.log(`%cXHR Response #${xhrId}:`, 'color: cyan; font-weight: bold;', `${responseDetails.status} ${responseDetails.statusText} ${responseDetails.responseType} \n`, responseDetails);

        }

        xhr.send = function(body) {
            activeXhrCount++;
            requestDetails.body = body;
            requestDetails.startTime = new Date();
            originalXMLHttpRequest.prototype.send.call(this, body);
        };

        return xhr;


        function downloadNetworkActivityLog() {
            const dataStr = JSON.stringify(window.networkActivityLog, null, 2);
            const blob = new Blob([dataStr], {type: "application/json"});
            const objectURL = URL.createObjectURL(blob);
            const downloadAnchorNode = document.createElement('a');
            downloadAnchorNode.setAttribute('href', objectURL);
            downloadAnchorNode.setAttribute('download', 'NetworkActivityLog.json');
            document.body.appendChild(downloadAnchorNode);
            downloadAnchorNode.click();
            downloadAnchorNode.remove();
        }

    }

    window.XMLHttpRequest = ModifiedXMLHttpRequest;
})();
